import { Header } from "@budibase/backend-core"
import * as setup from "../../api/routes/tests/utilities"
import { setEnv } from "../../environment"
import {
  WorkspaceMigration,
  checkMissingMigrations,
  getLatestEnabledMigrationId,
} from "../index"
import * as migrations from "../migrations"
import {
  getWorkspaceMigrationVerions,
  updateWorkspaceMigrationMetadata,
} from "../workspaceMigrationMetadata"

jest.mock<typeof migrations>("../migrations", () => ({
  MIGRATIONS: [
    {
      id: "20231211101320_test",
      func: migrationLogic(),
    },
  ],
}))

function migrationLogic(executionMS = 0): () => Promise<void> {
  return () => new Promise(r => setTimeout(r, executionMS))
}

describe("migrations", () => {
  beforeAll(() => setEnv({ DISABLE_WORKSPACE_MIGRATIONS: false }))

  it("new workspaces are created with the latest workspace migration version set", async () => {
    const config = setup.getConfig()
    await config.init()

    await config.doInContext(config.getDevWorkspaceId(), async () => {
      const migrationVersion = await getWorkspaceMigrationVerions(
        config.getDevWorkspaceId()
      )

      expect(migrationVersion).toEqual("20231211101320_test")
    })
  })

  it("accessing a workspace that has no pending migrations will not attach the migrating header", async () => {
    const config = setup.getConfig()
    await config.init()

    const appId = config.getDevWorkspaceId()

    await config.api.workspace.get(appId, {
      headersNotPresent: [Header.MIGRATING_APP],
    })
  })

  it("accessing a workspace that has pending migrations will attach the migrating header", async () => {
    const config = setup.getConfig()
    await config.init()

    const appId = config.getDevWorkspaceId()

    migrations.MIGRATIONS.push({
      id: "20231211105812_new-test",
      func: migrationLogic(5000),
    })

    await config.api.workspace.get(appId, {
      headers: {
        [Header.MIGRATING_APP]: appId,
      },
    })
  })

  it("should disable all migrations after one that is disabled", () => {
    const MIGRATION_ID1 = "20231211105810_new-test",
      MIGRATION_ID2 = "20231211105812_new-test",
      MIGRATION_ID3 = "20231211105814_new-test"
    // create some migrations to test with
    const migrations: WorkspaceMigration[] = [
      {
        id: MIGRATION_ID1,
        func: migrationLogic(),
      },
      {
        id: MIGRATION_ID2,
        func: migrationLogic(),
      },
      {
        id: MIGRATION_ID3,
        func: migrationLogic(),
      },
    ]

    expect(getLatestEnabledMigrationId(migrations)).toBe(MIGRATION_ID3)
    migrations[1].disabled = true
    expect(getLatestEnabledMigrationId(migrations)).toBe(MIGRATION_ID1)
  })

  describe("index-based migration comparison", () => {
    beforeEach(() => {
      // Reset migrations array to known state
      migrations.MIGRATIONS.length = 0
      migrations.MIGRATIONS.push(
        { id: "20231211101320_test", func: migrationLogic(1000) },
        { id: "20231211101330_test2", func: migrationLogic(1000) },
        { id: "20231211101340_test3", func: migrationLogic(1000) }
      )
    })

    it("should trigger migration when there are future migrations based on the current version", async () => {
      const config = setup.getConfig()
      await config.init()
      const workspaceId = config.getDevWorkspaceId()

      // Set app to first migration
      await config.doInContext(workspaceId, async () => {
        await updateWorkspaceMigrationMetadata({
          workspaceId,
          version: "20231211101320_test",
        })
      })

      const mockNext = jest.fn()
      const ctx = { response: { set: jest.fn() } } as any

      await config.doInContext(workspaceId, () =>
        checkMissingMigrations(ctx, mockNext, workspaceId)
      )

      expect(ctx.response.set).toHaveBeenCalledWith(
        Header.MIGRATING_APP,
        workspaceId
      )
      expect(mockNext).toHaveBeenCalled()
    })

    it("should not trigger migration when current version is latest", async () => {
      const config = setup.getConfig()
      await config.init()
      const workspaceId = config.getDevWorkspaceId()

      // Set app to latest migration
      await config.doInContext(workspaceId, async () => {
        await updateWorkspaceMigrationMetadata({
          workspaceId,
          version: "20231211101340_test3",
        })
      })

      const mockNext = jest.fn()
      const ctx = { response: { set: jest.fn() } } as any

      await config.doInContext(workspaceId, () =>
        checkMissingMigrations(ctx, mockNext, workspaceId)
      )

      expect(ctx.response.set).not.toHaveBeenCalled()
      expect(mockNext).toHaveBeenCalled()
    })

    it("should trigger migration when current version not found in array", async () => {
      const config = setup.getConfig()
      await config.init()
      const workspaceId = config.getDevWorkspaceId()

      // Set app to non-existent migration
      await config.doInContext(workspaceId, async () => {
        await updateWorkspaceMigrationMetadata({
          workspaceId,
          version: "nonexistent_migration",
        })
      })

      const mockNext = jest.fn()
      const ctx = { response: { set: jest.fn() } } as any

      await config.doInContext(workspaceId, () =>
        checkMissingMigrations(ctx, mockNext, workspaceId)
      )

      expect(ctx.response.set).toHaveBeenCalledWith(
        Header.MIGRATING_APP,
        workspaceId
      )
      expect(mockNext).toHaveBeenCalled()
    })
  })

  it("should handle rapid migration completion", async () => {
    migrations.MIGRATIONS.length = 0
    migrations.MIGRATIONS.push({
      id: "20250626103320_test",
      func: migrationLogic(),
    })

    const config = setup.getConfig()
    await config.init()
    const appId = config.getDevWorkspaceId()

    const mockNext = jest.fn()
    const ctx = { response: { set: jest.fn() } } as any

    await config.doInContext(appId, () =>
      checkMissingMigrations(ctx, mockNext, appId)
    )

    expect(ctx.response.set).not.toHaveBeenCalled()
    expect(mockNext).toHaveBeenCalled()
  })

  describe("workspace existence check", () => {
    beforeEach(() => {
      migrations.MIGRATIONS.length = 0
      migrations.MIGRATIONS.push({
        id: "20231211101320_test",
        func: migrationLogic(),
      })
    })

    it("should skip migrations when workspace does not exist", async () => {
      const config = setup.getConfig()
      await config.init()

      // Use a non-existent app ID
      const nonExistentAppId = "app_nonexistent123456"

      const mockNext = jest.fn()
      const ctx = { response: { set: jest.fn() } } as any

      await checkMissingMigrations(ctx, mockNext, nonExistentAppId)

      expect(ctx.response.set).not.toHaveBeenCalled()
      expect(mockNext).toHaveBeenCalled()
    })
  })
})
